// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.gradle.service.resolve.transformation

import com.intellij.lang.java.beans.PropertyKind
import com.intellij.psi.*
import com.intellij.psi.impl.light.LightMethodBuilder
import com.intellij.psi.scope.PsiScopeProcessor
import com.intellij.psi.util.InheritanceUtil
import com.intellij.psi.util.PropertyUtilBase.*
import com.intellij.util.asSafely
import org.jetbrains.plugins.gradle.config.isGradleFile
import org.jetbrains.plugins.gradle.service.resolve.GradleCommonClassNames
import org.jetbrains.plugins.groovy.lang.resolve.NonCodeMembersContributor
import org.jetbrains.plugins.groovy.lang.resolve.getName
import org.jetbrains.plugins.groovy.lang.resolve.shouldProcessMethods
import org.jetbrains.plugins.groovy.lang.resolve.shouldProcessProperties

/**
 * For each getter with type [org.gradle.api.provider.Property], Gradle generates a corresponding setter
 * @see org.gradle.internal.instantiation.generator.AbstractClassGenerator
 */
class GradleSyntheticSetterMemberContributor : NonCodeMembersContributor() {
  override fun getClassNames(): Collection<String> = emptyList()

  override fun getParentClassName(): String? = null

  override fun processDynamicElements(qualifierType: PsiType,
                                      aClass: PsiClass?,
                                      processor: PsiScopeProcessor,
                                      place: PsiElement,
                                      state: ResolveState) {

    if (aClass == null) {
      return
    }
    if (place.containingFile.isGradleFile().not()) {
      return
    }
    if (!(processor.shouldProcessMethods() || processor.shouldProcessProperties())) {
      return
    }
    val nameHint = processor.getName(state)?.let { getPropertyName(it) ?: return } ?: ""
    val getters = getAllProperties(aClass, false, true, true)
    for ((simpleName, method) in getters) {
      if (nameHint !in simpleName) {
        continue
      }
      processSetter(method, simpleName, processor, state)
    }
  }
}


/**
 * Hardcoded set of classes for which there is a synthetic setter generated
 */
private val assignmentPermittedClasses = listOf(
  GradleCommonClassNames.GRADLE_API_PROVIDER_PROPERTY,
  GradleCommonClassNames.GRADLE_API_FILE_CONFIGURABLE_FILE_COLLECTION,
  GradleCommonClassNames.GRADLE_API_PROVIDER_MAP_PROPERTY,
  GradleCommonClassNames.GRADLE_API_PROVIDER_HAS_MULTIPLE_VALUES,
)

private fun processSetter(method: PsiMethod, simpleName : String, processor: PsiScopeProcessor, state: ResolveState) {
  if (!isSimplePropertyGetter(method)) {
    return
  }
  val returnType = method.returnType.asSafely<PsiClassType>() ?: return
  val resolvedResult = returnType.resolveGenerics()
  val resolvedClass = resolvedResult.element ?: return
  if (!assignmentPermittedClasses.any { InheritanceUtil.isInheritor(resolvedClass, it) } && !hasGeneratedAssignmentInKotlin(
      resolvedClass)) {
    return
  }
  val setterName = getAccessorName(simpleName, PropertyKind.SETTER)
  val backingSetters = resolvedClass.findMethodsAndTheirSubstitutorsByName("set", true) + resolvedClass.findMethodsAndTheirSubstitutorsByName("setFrom", true)
  for (backingSetter in backingSetters) {
    val generatedSetter = generateSetter(backingSetter.first, resolvedResult.substitutor, backingSetter.second, method, setterName)
    processor.execute(generatedSetter, state)
  }
}

private fun generateSetter(backingMethod: PsiMethod, deepSubstitutor: PsiSubstitutor, substitutor: PsiSubstitutor, originMethod: PsiMethod, setterName: String) : PsiMethod = LightMethodBuilder(backingMethod.manager, setterName).apply {
  navigationElement = originMethod
  containingClass = originMethod.containingClass
  originInfo = "Generated by decoration of Gradle property-like class"
  setMethodReturnType(PsiTypes.voidType())
  for (backingParameter in backingMethod.parameterList.parameters) {
    addParameter(backingParameter.name, deepSubstitutor.substitute(substitutor.substitute(backingParameter.type)))
  }
}

private fun hasGeneratedAssignmentInKotlin(clazz : PsiClass) : Boolean {
  val superClasses = arrayOf(clazz) + clazz.supers
  return superClasses.any {superClass ->
    superClass.annotations.any { it.hasQualifiedName(GradleCommonClassNames.GRADLE_API_SUPPORTS_KOTLIN_ASSIGNMENT_OVERLOADING) }
  }
}